package com.hmdp.service.impl;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.SystemConstants;
import io.micrometer.common.util.StringUtils;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.domain.geo.GeoReference;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.*;

/**
 * <p>
 *  服务实现类
 * </p>
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 根据id查询商铺信息--使用redis缓存处理
     * @param id
     * @return
     */
    @Override
    public Result queryById(Long id) {
        //1.从redis中查询商铺缓存，存储结构：
        String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        //2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            //3.存在，直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);//转换为java对象
            return Result.ok(shop);
        }
        /**
         * 业务优化2：解决缓存穿透--数据库中也不存在时，将空值写入redis，
         * 此后先判断redis中查出的值是否为空，若为空，则表示数据库中不存在该店铺，直接返回错误，避免多次再查询数据库，减轻数据库的压力
         */
        if (shopJson != null) {
            return Result.fail("店铺不存在");
        }
//4.不存在，根据id查询数据库
        Shop shop = getById(id);//maybatis-plus的查询数据库
        if (shop == null) {
            //5.数据库不存在，返回错误
//原：            return Result.fail("店铺不存在");
            /**
             * 业务优化2：解决缓存穿透--数据库中也不存在时，将空值写入redis，此后先判断redis中查出的值是否为空，
             * 若为空，则表示数据库中不存在该店铺，直接返回错误，不必再查询数据库，减轻数据库的压力
             */
            //写入redis，opsForValue--数据结构为string，值为空字符串
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_SHOP_TTL, TimeUnit.MINUTES);
        }
        //6.数据库存在，写入redis
        /**
         * 业务优化1：保证一致性--缓存设置超时时间
         */
    //原    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop));
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        //7.返回结果
        return Result.ok(shop);

    }

    /**业务优化1：保证一致性--更新数据库时，删除redis中的商铺缓存
     * 更新商铺信息--一致性--数据库更新时，删除redis中的商铺缓存，保证数据库和redis中的数据一致性
     * @param shop
     * @return
     */
    @Override
    public Result updateShop(Shop shop) {
        Long id = shop.getId();
        if (id == null) {
            return Result.fail("店铺id不能为空");
        }
        //1.更新数据库
        updateById(shop);
        //2.删除redis中的商铺缓存
        stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
        return Result.ok();
    }


    //根据类型id，坐标查询商铺信息
    @Override
    public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
        // 1.判断用户是否传入当前坐标，根据坐标查
        if (x == null || y == null) {
            // 不需要坐标查询，按数据库查询--mybatis-plus分页查询
            Page<Shop> page = query()
                    .eq("type_id", typeId)
                    .page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
            // 返回数据
            return Result.ok(page.getRecords());
        }

        //需要根据传入的坐标查询，按距离排序，在redis中分页查询

        // 2.计算分页参数，current前端传来的页码，from end是本次分页的起始和结束索引
        int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
        int end = current * SystemConstants.DEFAULT_PAGE_SIZE;

        // 3.查询redis、按照距离排序、分页。
        //数据结构：以shoptype为key（1，2），存入经纬度和shopid.其中shopid是Member字段，经纬度计算出一个52位的数值作为Score分数
        //search命令 根据key，和传入的用户坐标，查询出距离用户5000m内 最近的店铺，0--前end个
        //结果result：
        // 1. GeoResult<RedisGeoCommands.GeoLocation<String>>
        // 2. RedisGeoCommands.GeoLocation<String> 中有Content字段，里面有name和coordinate，name是shopid，coordinate是经纬度
        // 结果：shopId、distance
        String key = SHOP_GEO_KEY + typeId;
        GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo() // GEOSEARCH key BYLONLAT x y BYRADIUS 10 WITHDISTANCE
                .search(
                        key,
                        GeoReference.fromCoordinate(x, y),
                        new Distance(5000),
                        RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end) //分页，从0开始，到end结束
                );
        // 4.解析出商铺id
        if (results == null) {
            return Result.ok(Collections.emptyList());
        }
        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();
        //List包含GeoResult<RedisGeoCommands.GeoLocation<String>>
        //GeoResult包含 {
        //    RedisGeoCommands.GeoLocation<String> content; // 地理位置数据
        //    Distance distance;                            // 与查询坐标的实际距离
        //}
        //其中GeoLocation
        //RedisGeoCommands.GeoLocation<String> {
//            String name;        // 存储的是店铺ID字符串（即之前存储的 member）
//            Point point;        // 店铺的经纬度坐标（x=经度，y=纬度）
//        }
        if (list.size() <= from) {
            // 没有下一页了，结束
            return Result.ok(Collections.emptyList());
        }
        // 4.1.截取 from ~ end的部分
        List<Long> ids = new ArrayList<>(list.size());
        Map<String, Distance> distanceMap = new HashMap<>(list.size());
        list.stream().skip(from).forEach(result -> {  //stream流 skip跳过前from个，forEach遍历后面的元素，实现分页查询本次from到end的元素
            // 4.2.获取店铺id
            String shopIdStr = result.getContent().getName(); //getName()方法返回的是Member字段，即shopid
            ids.add(Long.valueOf(shopIdStr));
            // 4.3.获取距离
            Distance distance = result.getDistance();
            distanceMap.put(shopIdStr, distance);
        });
        // 5.根据筛选出的店铺id查询Shop对象
        String idStr = StrUtil.join(",", ids);
        List<Shop> shops = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();
        for (Shop shop : shops) {
            shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
        }
        // 6.返回
        return Result.ok(shops);
    }







    /**以下详细见文档--两种方案解决缓存击穿
     * 拟造--使用redis中的setnx实现互斥锁--
     * 该方法含义是redis中如果没有这个key，则插入成功，返回1，在stringRedisTemplate中返回true，
     * 如果有这个key则插入失败，则返回0，在stringRedisTemplate返回false，
     * 我们可以通过true，或者是false，来表示是否有线程成功插入key，成功插入的key的线程我们认为他就是获得到锁的线程
     */
    //上锁
    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        //setIfAbsent方法是redis中的setnx方法
        return BooleanUtil.isTrue(flag); //将Boolean类型转换为boolean类型
    }
    //解锁
    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }
}
